/**********************************
 * Assignment 5: Alien vs Predator
 * Date: 12/04/2011
 *
 * Alexander Miller: ahm82, 2605797
 * David Felty: djf242, 2828357
 * David Hau: dch229, 2586217
 * Hanna Atmer: ha237, 2666426
 **********************************/

package avp;

import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import javax.swing.JFrame;

import edu.cornell.cs.cs2110.RandomBag;

public class GameEngine {

	public boolean gameOver = false;
	public boolean useGUI = true;
	
	// Current graph of the ship
	private Ship ship = new Ship(GameConstants.WIDTH, GameConstants.HEIGHT);
	
	// Graphical interface
	private GUI gui;
	
	// Alien and predator objects
	private Agent alien = new Alien();
	private Agent predator = new Predator();
	
	// Points to the alien or predator if it has found the scanner, null otherwise
	private Agent scannerOwner;
	
	// The last move returned by each agent's last nextMove() call
	private Move alienLastMove;
	private Move predatorLastMove;
	
	private Action alienAction;
	private Action predatorAction;
	
	private int alienActionProgress;
	private int predatorActionProgress;
	
	// Each turn consists of GameConstants.SPEED_RATIO ticks
	private int turnProgress = 2;
	
	private enum Action {
		MOVE, OPEN, CLOSE, BURN, MOVE_AND_CLOSE, SKIP, INVALID
	}
	
	
	public GameEngine() {
		// Set a random node as the scanner's location
		Random rand = new Random();
		int row = rand.nextInt(GameConstants.HEIGHT);
		int col = rand.nextInt(GameConstants.WIDTH);
		ship.scannerNode = ship.grid[row][col];
		scannerOwner = null;
		
		// Set a random node as the control room
		row = rand.nextInt(GameConstants.HEIGHT);
		col = rand.nextInt(GameConstants.WIDTH);
		ship.controlNode = ship.grid[row][col];

		// Set a random node far away from the control room as the predator's location
		row = randomFarPosition(row, GameConstants.HEIGHT);
		col = randomFarPosition(col, GameConstants.WIDTH);
		ship.predatorNode = ship.grid[row][col];
		
		// Set a random node far away from the predator's location as the alien's location
		row = randomFarPosition(row, GameConstants.HEIGHT);
		col = randomFarPosition(col, GameConstants.WIDTH);
		ship.alienNode = ship.grid[row][col];
	}
	
	
	// Returns a random integer at least max/3 positions away from x
	private int randomFarPosition(int x, int max) {
		int r = (int)(Math.random() * max/3);
		return (x + max/3 + r) % max;
	}
	
	
	// Initializes agents and shows the GUI
	public void init() {
		if (useGUI) gui = new GUI(ship);
		
		// Add walls
		Set<Edge> leftovers = new HashSet<Edge>(ship.getEdges());
		ship.spanningTree();
		// Get the edges that were removed...
		leftovers.removeAll(ship.getEdges());
		RandomBag<Edge> randEdges = new RandomBag<Edge>();
		for (Edge e : leftovers) {
			randEdges.insert(e);
		}
		// and add some back to the graph
		int extra = GameConstants.EXTRA_EDGES;
		for (Edge e : randEdges) {
			e.state = EdgeState.OPEN;
			ship.addEdge(e);
			if (--extra == 0) break;
		}
		
		Ship s = ship.clone();
		s.predatorNode = s.grid[ship.predatorNode.row][ship.predatorNode.col];
		s.controlNode  = s.grid[ship.controlNode.row][ship.controlNode.col];
		predator.init(s);
		alien.init(null);
		
		getPredatorMove();
		getAlienMove();
		
		if (useGUI) {
			gui.repaint();
			gui.setVisible(true);
			gui.setExtendedState(gui.getExtendedState() | JFrame.MAXIMIZED_BOTH);
		}
	}
	
	// Performs the time tick, advancing the alien and predator 1 time unit
	// Returns true if a character has won the game
	public void nextTick() {
		// Execute a tick of the predator's turn if he is ready
		turnProgress = (turnProgress + 1) % GameConstants.SPEED_RATIO;
		if (turnProgress == 0) {
			predatorActionProgress--;
			if (predatorActionProgress == 0) {
				if (predatorAction == Action.OPEN || predatorAction == Action.CLOSE) executePredatorAction();
				if (gameOver) {
					if (useGUI) gui.repaint();
					return;
				}
				getPredatorMove();
				if (predatorActionProgress == 1) executePredatorAction();
			}
		}
		
		// Execute a tick of the alien's turn
		alienActionProgress--;
		if (alienActionProgress == 0) {
			if (alienAction == Action.BURN) executeAlienAction();
			getAlienMove();
			if (alienActionProgress == 1) executeAlienAction();
		}
		
		if (useGUI) gui.repaint();
	}
	
	public void quit() {
		if (useGUI) gui.setVisible(false);
	}
	
	private void checkWin() {
		Node a = ship.alienNode, p = ship.predatorNode, c = ship.controlNode;
		if (a == p) {
			if (useGUI) {
				gui.repaint();
				gui.setMessage("Alien wins!");
			}
			Main.alienWins++;
			gameOver = true;
		}
		if (p == c  && ship.getPath(p, a) == null) {
			if (useGUI) {
				gui.repaint();
				gui.setMessage("Predator wins!");
			}
			Main.predatorWins++;
			gameOver = true;
		}
	}

	
	// Get the predator's next move and determine his action
	private void getPredatorMove() {
		// Prepare info
		Info i = new Info();
		i.location = ship.predatorNode;
		i.allEdges = ship.predatorNode.allEdges();
		i.adjacentEdges = ship.predatorNode.getAdjacent();
		i.atControlRoom = (ship.predatorNode == ship.controlNode);
		i.atScanner = (predator == scannerOwner);
		if (i.atScanner) i.ship = ship;
		List<Edge> path = ship.getPath(ship.predatorNode, ship.alienNode);
		if (path != null && path.size() > 0 && path.size() <= GameConstants.PRED_SENSE) {
			i.adversaryPresent = true;
			i.adversaryDirection = path.get(0);
		}
		
		// Get move
		predatorLastMove = predator.nextMove(i);
		Action act = getPredatorAction();
		
		// Log invalid move
		if (act == Action.INVALID) {
			log(predator, predatorLastMove);
		} else if (act != Action.SKIP && !agentGivenEdge(predatorLastMove.edge, ship.predatorNode.allEdges())) {
			log(predator, "returned edge wasn't supplied to predator. STATE: " + ((Predator)predator).getState());
			act = Action.INVALID;
		}
		predatorAction = act;
		
		// Determine time action will take
		// I assume it takes equal amounts of time to open and close a door while standing still
		predatorActionProgress = 
				(act == Action.OPEN || act == Action.CLOSE ? GameConstants.OPEN_TIME : 1);
	}
	
	// Determine action
	private Action getPredatorAction() {
		Move m = predatorLastMove;
		Action act = Action.INVALID;
		
		if (m != null) {
			if (m.edge == null && !m.move && !m.changeState) {
				act = Action.SKIP;
			} else if (m.edge != null) {
				EdgeState s = m.edge.state;
				if (m.move && m.changeState && s == EdgeState.OPEN) {
					act = Action.MOVE_AND_CLOSE;
				} else if (m.move && !m.changeState && (s == EdgeState.OPEN || s == EdgeState.BURNED)) {
					act = Action.MOVE;
				} else if (!m.move && m.changeState) {
					if (s == EdgeState.OPEN)        act = Action.CLOSE;
					else if (s == EdgeState.CLOSED) act = Action.OPEN;
				}
			}
		}
		return act;
	}
	
	
	// Get the alien's next move and determine his action
	private void getAlienMove() {
		// Prepare info
		Info i = new Info();
		i.location = ship.alienNode;
		i.allEdges = ship.alienNode.allEdges();
		i.adjacentEdges = ship.alienNode.getAdjacent();
		i.atScanner = (alien == scannerOwner);
		if (i.atScanner) i.ship = ship;
		List<Edge> path = ship.getPath(ship.alienNode, ship.predatorNode);
		if (path != null && path.size() > 0 && path.size() <= GameConstants.ALIEN_SENSE) {
			i.adversaryPresent = true;
			i.adversaryDirection = path.get(0);
		}
		for (Edge e : ship.alienNode.allEdges()) {
			if (e.nodes.contains(ship.predatorNode)) {
				i.adversaryThruWall = e;
				break;
			}
		}
		
		// Get move
		alienLastMove = alien.nextMove(i);
		Action act = getAlienAction();
		
		// Log invalid move
		if (act == Action.INVALID) {
			log(alien, alienLastMove);
		} else if(!agentGivenEdge(alienLastMove.edge, ship.alienNode.allEdges())) {
			log(alien, "returned edge wasn't supplied to alien. STATE: " + ((Alien)alien).getState());
			act = Action.INVALID;
		}
		alienAction = act;
		
		// Determine time action will take
		alienActionProgress = (act == Action.BURN ? GameConstants.BURN_TIME : 1);
	}
	
	private Action getAlienAction() {
		Move m = alienLastMove;
		Action act = Action.INVALID;
		
		// Determine action
		if (m != null) {
			if (m.edge == null && !m.move && !m.changeState) {
				act = Action.SKIP;
			} else if (m.edge != null) {
				EdgeState s = m.edge.state;
				if (m.move && !m.changeState && (s == EdgeState.OPEN || s == EdgeState.BURNED)) {
					act = Action.MOVE;
				} else if (!m.move && m.changeState && s != EdgeState.BURNED) {
					act = Action.BURN;
				}
			}
		}
		return act;
	}
	
	// Executes the predator's next action based on predatorAction.
	private void executePredatorAction() {
		Move m = predatorLastMove;
		switch (predatorAction) {
		case MOVE:
			ship.predatorNode = ship.predatorNode.getOtherThru(m.edge);
			break;
		case OPEN:
			m.edge.state = EdgeState.OPEN;
			break;
		case CLOSE:
			m.edge.state = EdgeState.CLOSED;
			break;
		case MOVE_AND_CLOSE:
			ship.predatorNode = ship.predatorNode.getOtherThru(m.edge);
			m.edge.state = EdgeState.CLOSED;
			break;
		case SKIP:
		case INVALID:
			break; // Wait a turn
		default:
			assert false;
		}
		if (ship.predatorNode == ship.scannerNode)  {
			if (useGUI) gui.setMessage("The predator has found the scanner!");
			scannerOwner = predator;
			ship.scannerNode = null;
		}
		checkWin();
	}

	// Executes the alien's next action based on alienAction.
	private void executeAlienAction() {
		Move m = alienLastMove;
		switch (alienAction) {
		case MOVE:
			ship.alienNode = ship.alienNode.getOtherThru(m.edge);
			if (ship.alienNode == ship.scannerNode) {
				if (useGUI) gui.setMessage("The alien has found the scanner!");
				scannerOwner = alien;
				ship.scannerNode = null;
			}
			break;
		case BURN:
			m.edge.state = EdgeState.BURNED;
			ship.addEdge(m.edge);
			break;
		case SKIP:
		case INVALID:
			break; // Wait a turn
		default:
			assert false;
		}
		checkWin();
	}
	
	// Tests that x equals another edge in es (in the sense of ==)
	private boolean agentGivenEdge(Edge x, Set<Edge> es) {
		for (Edge e : es) {
			if (x == e) return true;
		}
		return false;
	}
	
	private void updateGUI() {
		if (useGUI) gui.repaint();
	}
	
	// Prints error in an incorrect move for the given agent
	private void log(Agent agent, Move m) {
		String msg;
		if (m == null) {
			msg = "null Move returned";
		} else if (m.edge == null) {
			msg = "edge = null, but " + (m.move ? "move = true" : "changeState = true");
		} else {
			msg = String.format("move = %b, changeState = %b, edge = (%s), edge.state = %s",
					m.move, m.changeState, m.edge, m.edge.state);
		}
		log(agent, msg);
	}
	
	// Prints error msg for the given agent
	private void log(Agent agent, Object msg) {
		String agt, loc;
		if (agent == alien) {
			agt = "alien";
			loc = ship.alienNode.toString();
		} else {
			agt = "predator";
			loc = ship.predatorNode.toString();
		}
		String s = String.format("Invalid move for %s at (%s): %s", agt, loc, msg);
		if (useGUI) gui.setMessage(s);
		System.out.println(s);
	}
	
}
